//The MIT License
//
//Copyright (c) 2009 nodchip
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.
package tv.dyndns.kishibe.server.database;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import tv.dyndns.kishibe.client.game.ProblemGenre;
import tv.dyndns.kishibe.client.game.ProblemType;
import tv.dyndns.kishibe.client.game.RandomFlag;
import tv.dyndns.kishibe.client.packet.PacketBbsResponse;
import tv.dyndns.kishibe.client.packet.PacketBbsThread;
import tv.dyndns.kishibe.client.packet.PacketChatData;
import tv.dyndns.kishibe.client.packet.PacketLinkData;
import tv.dyndns.kishibe.client.packet.PacketProblem;
import tv.dyndns.kishibe.client.packet.PacketProblemCreationLog;
import tv.dyndns.kishibe.client.packet.PacketProblemMinimum;
import tv.dyndns.kishibe.client.packet.PacketRankingData;
import tv.dyndns.kishibe.client.packet.PacketThemeModeEditor;
import tv.dyndns.kishibe.client.packet.PacketThemeModeEditor.ThemeModeEditorStatus;
import tv.dyndns.kishibe.client.packet.PacketUserData;
import tv.dyndns.kishibe.client.packet.PacketWrongAnswer;
import tv.dyndns.kishibe.server.PageView;
import tv.dyndns.kishibe.server.ThreadPool;
import tv.dyndns.kishibe.server.util.IntArray;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheStats;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.name.Named;

public class CachedDatabase implements Database {
	private static final Logger logger = Logger.getLogger(CachedDatabase.class.toString());
	private static final boolean ENABLE_CACHE_STATS = true;
	@VisibleForTesting
	final Database database;

	// //////////////////////////////////////////////////////////////////////////////
	// キャッシュ
	@Inject
	public CachedDatabase(@Named("DirectDatabase") Database database, ThreadPool threadPool) {
		this.database = database;

		threadPool.addHourTask(new Runnable() {
			public void run() {
				try {
					updateNumberOfActiveUsers();
					updateRankingData();
					updateThemeModeRaking();
					updateLastestProblems();
				} catch (DatabaseException e) {
					logger.log(Level.WARNING, "キャッシュの更新に失敗しました", e);
				}
			}
		});

		threadPool.addMinuteTasks(new Runnable() {
			public void run() {
				try {
					updateIgnoreServerUserCode();
				} catch (DatabaseException e) {
					logger.log(Level.WARNING, "キャッシュの更新に失敗しました", e);
				}
			}
		});

		if (ENABLE_CACHE_STATS) {
			threadPool.addDailyTask(new Runnable() {
				@Override
				public void run() {
					writeCacheStatsToLog();
				}
			});
		}
	}

	private void writeCacheStatsToLog() {
		for (Entry<String, Cache<?, ?>> e : caches.entrySet()) {
			String name = e.getKey();
			CacheStats stats = e.getValue().stats();
			logger.log(
					Level.INFO,
					name + " " + stats.toString() + " averageLoadPenalty="
							+ stats.averageLoadPenalty());
		}
	}

	@Override
	public void addChatLog(PacketChatData data) throws DatabaseException {
		database.addChatLog(data);
	}

	@Override
	public Map<Integer, PacketChatData> getLatestChatData() throws DatabaseException {
		return database.getLatestChatData();
	}

	@Override
	public void addPlayerAnswers(int problemID, ProblemType type, List<String> answers)
			throws DatabaseException {
		database.addPlayerAnswers(problemID, type, answers);
	}

	@Override
	public void addProblemIdsToReport(int userCode, List<Integer> problemIds)
			throws DatabaseException {
		database.addProblemIdsToReport(userCode, problemIds);
	}

	@Override
	public void addRatingHistory(int userCode, int rating) throws DatabaseException {
		database.addRatingHistory(userCode, rating);
	}

	@Override
	public void clearProblemFeedback(int problemId) throws DatabaseException {
		database.clearProblemFeedback(problemId);
	}

	@Override
	public void clearProblemIdFromReport(int userCode) throws DatabaseException {
		database.clearProblemIdFromReport(userCode);
	}

	@Override
	public List<PacketWrongAnswer> getPlayerAnswers(int problemID) throws DatabaseException {
		return database.getPlayerAnswers(problemID);
	}

	@Override
	public List<String> getProblemFeedback(int problemId) throws DatabaseException {
		return database.getProblemFeedback(problemId);
	}

	@Override
	public Map<Integer, List<Integer>> getRatingGroupedByPrefecture() throws DatabaseException {
		return database.getRatingGroupedByPrefecture();
	}

	@Override
	public List<Integer> getRatingHistory(int userCode) throws DatabaseException {
		return database.getRatingHistory(userCode);
	}

	@Override
	public List<PacketProblem> getUserProblemReport(int userCode) throws DatabaseException {
		return database.getUserProblemReport(userCode);
	}

	@Override
	public List<Integer> getWholeRating() throws DatabaseException {
		return database.getWholeRating();
	}

	@Override
	public boolean isUsedUserCode(int userCode) throws DatabaseException {
		return database.isUsedUserCode(userCode);
	}

	@Override
	public PageView loadPageView() throws DatabaseException {
		return database.loadPageView();
	}

	@Override
	public void removePlayerAnswers(int problemID) throws DatabaseException {
		database.removePlayerAnswers(problemID);
	}

	@Override
	public void removeProblemIdFromReport(int userCode, int problemID) throws DatabaseException {
		database.removeProblemIdFromReport(userCode, problemID);
	}

	@Override
	public void savePageView(PageView pageView) throws DatabaseException {
		database.savePageView(pageView);
	}

	@Override
	public void updateThemeModeScore(int userCode, String theme, int score)
			throws DatabaseException {
		database.updateThemeModeScore(userCode, theme, score);
	}

	@Override
	public void voteToProblem(int problemId, boolean good, String feedback)
			throws DatabaseException {
		database.voteToProblem(problemId, good, feedback);
	}

	@Override
	public void resetVote(int problemId) throws DatabaseException {
		database.resetVote(problemId);
	}

	@Override
	public int getNumberOfCreationLogWithUserCode(int userCode, long dateFrom)
			throws DatabaseException {
		return database.getNumberOfCreationLogWithUserCode(userCode, dateFrom);
	}

	@Override
	public int getNumberOfCreationLogWithMachineIp(String machineIp, long dateFrom)
			throws DatabaseException {
		return database.getNumberOfCreationLogWithMachineIp(machineIp, dateFrom);
	}

	@Override
	public int getChatLogId(int year, int month, int day, int hour, int minute, int second)
			throws DatabaseException {
		return database.getChatLogId(year, month, day, hour, minute, second);
	}

	@Override
	public List<PacketChatData> getChatLog(int start) throws DatabaseException {
		return database.getChatLog(start);
	}

	@Override
	public int getNumberOfChatLog() throws DatabaseException {
		return database.getNumberOfChatLog();
	}

	@Override
	public List<PacketProblem> getIndicatedProblems() throws DatabaseException {
		return database.getIndicatedProblems();
	}

	private final Map<String, Cache<?, ?>> caches = Maps.newHashMap();

	private <K, V> LoadingCache<K, V> build(String name, CacheLoader<K, V> loader) {
		CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder().softValues()
				.expireAfterAccess(1, TimeUnit.HOURS);
		if (ENABLE_CACHE_STATS) {
			builder.recordStats();
		}
		LoadingCache<K, V> cache = builder.build(loader);
		caches.put(name, cache);
		return cache;
	}

	// //////////////////////////////////////////////////////////////////////////////
	// ユーザーデータ
	private final LoadingCache<Integer, PacketUserData> userDataCache = build("userDataCache",
			new CacheLoader<Integer, PacketUserData>() {
				@Override
				public PacketUserData load(Integer key) throws Exception {
					return database.getUserData(key);
				}
			});

	public PacketUserData getUserData(int userCode) throws DatabaseException {
		try {
			return userDataCache.get(userCode);
		} catch (ExecutionException e) {
			throw new DatabaseException(e);
		}
	}

	public void setUserData(PacketUserData data) throws DatabaseException {
		database.setUserData(data);
		userDataCache.invalidate(data.userCode);
		numberOfActiveUsers = 0;
	}

	public void removeIgnoreUserCode(int userCode, int targetUserCode) throws DatabaseException {
		database.removeIgnoreUserCode(userCode, targetUserCode);
		userDataCache.invalidate(userCode);
	}

	public void addIgnoreUserCode(int userCode, int targetUserCode) throws DatabaseException {
		database.addIgnoreUserCode(userCode, targetUserCode);
		userDataCache.invalidate(userCode);
	}

	// //////////////////////////////////////////////////////////////////////////////
	// ランキング
	private final Object lockRankingData = new Object();
	private volatile List<List<PacketRankingData>> rankingData;

	public List<List<PacketRankingData>> getRankingData() throws DatabaseException {
		// 初期値チェックのみで良いためダブルチェック
		if (rankingData == null) {
			synchronized (lockRankingData) {
				if (rankingData == null) {
					rankingData = database.getRankingData();
				}
			}
		}
		return rankingData;
	}

	private void updateRankingData() throws DatabaseException {
		rankingData = database.getRankingData();
	}

	// //////////////////////////////////////////////////////////////////////////////
	// サーバー全体に適用される無視コード
	private final Object lockServerIgnoreUserCode = new Object();
	private volatile Set<Integer> serverIgnoreUserCode;

	public Set<Integer> getServerIgnoreUserCode() throws DatabaseException {
		// 初期値チェックのみで良いためダブルチェック
		if (serverIgnoreUserCode == null) {
			synchronized (lockServerIgnoreUserCode) {
				if (serverIgnoreUserCode == null) {
					serverIgnoreUserCode = database.getServerIgnoreUserCode();
				}
			}
		}
		return serverIgnoreUserCode;
	}

	@Override
	public void addServerIgnoreUserCode(int userCode) throws DatabaseException {
		database.addServerIgnoreUserCode(userCode);
		serverIgnoreUserCode = database.getServerIgnoreUserCode();
	}

	private void updateIgnoreServerUserCode() throws DatabaseException {
		serverIgnoreUserCode = database.getServerIgnoreUserCode();
	}

	// //////////////////////////////////////////////////////////////////////////////
	// 制限ユーザーコード
	private final Object lockLimitedUserCodes = new Object();
	private final Object lockLimitedRemoteAddresses = new Object();
	private volatile Set<Integer> limitedUserCodes = null;
	private volatile Set<String> limitedRemoteAddresses = null;

	public Set<Integer> getLimitedUserCodes() throws DatabaseException {
		if (limitedUserCodes == null) {
			synchronized (lockLimitedUserCodes) {
				if (limitedUserCodes == null) {
					limitedUserCodes = database.getLimitedUserCodes();
				}
			}
		}
		return limitedUserCodes;
	}

	public Set<String> getLimitedRemoteAddresses() throws DatabaseException {
		if (limitedRemoteAddresses == null) {
			synchronized (lockLimitedRemoteAddresses) {
				if (limitedRemoteAddresses == null) {
					limitedRemoteAddresses = database.getLimitedRemoteAddresses();
				}
			}
		}
		return limitedRemoteAddresses;
	}

	@Override
	public void addLimitedRemoteAddress(String remoteAddress) throws DatabaseException {
		database.addLimitedRemoteAddress(remoteAddress);
		limitedRemoteAddresses = database.getLimitedRemoteAddresses();
	}

	@Override
	public void addLimitedUserCode(int userCode) throws DatabaseException {
		database.addLimitedUserCode(userCode);
		limitedUserCodes = database.getLimitedUserCodes();
	}

	@Override
	public void removeLimitedUserCode(int userCode) throws DatabaseException {
		database.removeLimitedUserCode(userCode);
		limitedUserCodes = database.getLimitedUserCodes();
	}

	@Override
	public void removeLimitedRemoteAddress(String remoteAddress) throws DatabaseException {
		database.removeLimitedRemoteAddress(remoteAddress);
		limitedRemoteAddresses = database.getLimitedRemoteAddresses();
	}

	@Override
	public void clearLimitedUserCodes() throws DatabaseException {
		database.clearLimitedUserCodes();
		limitedUserCodes = database.getLimitedUserCodes();
	}

	@Override
	public void clearLimitedRemoteAddresses() throws DatabaseException {
		database.clearLimitedRemoteAddresses();
		limitedRemoteAddresses = database.getLimitedRemoteAddresses();
	}

	// キャッシュがヒットしないためキャッシュ化解除
	public List<PacketProblemCreationLog> getProblemCreationHistory(int problemId)
			throws DatabaseException {
		return database.getProblemCreationHistory(problemId);
	}

	public void addCreationLog(int problemId, int userCode, String machineIP)
			throws DatabaseException {
		database.addCreationLog(problemId, userCode, machineIP);
	}

	// //////////////////////////////////////////////////////////////////////////////
	// 掲示板
	private class BbsThreadCacheKey {
		private final int bbsId;
		private final int start;
		private final int count;

		private BbsThreadCacheKey(int bbsId, int start, int count) {
			this.bbsId = bbsId;
			this.start = start;
			this.count = count;
		}

		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof BbsThreadCacheKey)) {
				return false;
			}
			BbsThreadCacheKey rh = (BbsThreadCacheKey) obj;
			return bbsId == rh.bbsId && start == rh.start && count == rh.count;
		}

		@Override
		public int hashCode() {
			return Objects.hashCode(bbsId, start, count);
		}
	}

	private final LoadingCache<BbsThreadCacheKey, List<PacketBbsThread>> bbsThreadCache = build(
			"bbsThreadCache", new CacheLoader<BbsThreadCacheKey, List<PacketBbsThread>>() {
				@Override
				public List<PacketBbsThread> load(BbsThreadCacheKey key) throws Exception {
					return database.getBbsThreads(key.bbsId, key.start, key.count);
				}
			});

	private class BbsResponseCacheKey {
		private final int threadId;
		private final int count;

		private BbsResponseCacheKey(int threadId, int count) {
			this.threadId = threadId;
			this.count = count;
		}

		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof BbsResponseCacheKey)) {
				return false;
			}
			BbsResponseCacheKey rh = (BbsResponseCacheKey) obj;
			return threadId == rh.threadId && count == rh.count;
		}

		@Override
		public int hashCode() {
			return Objects.hashCode(threadId, count);
		}
	}

	private final LoadingCache<BbsResponseCacheKey, List<PacketBbsResponse>> bbsResponseCache = build(
			"bbsResponseCache", new CacheLoader<BbsResponseCacheKey, List<PacketBbsResponse>>() {
				@Override
				public List<PacketBbsResponse> load(BbsResponseCacheKey key) throws Exception {
					return database.getBbsResponses(key.threadId, key.count);
				}
			});
	/**
	 * bbsId -> numberOfBbsThreads
	 */
	private final LoadingCache<Integer, Integer> numberOfBbsThreadsCache = build(
			"numberOfBbsThreadsCache", new CacheLoader<Integer, Integer>() {
				@Override
				public Integer load(Integer key) throws Exception {
					return database.getNumberOfBbsThread(key);
				}
			});

	public List<PacketBbsThread> getBbsThreads(int bbsId, int start, int count)
			throws DatabaseException {
		try {
			return bbsThreadCache.get(new BbsThreadCacheKey(bbsId, start, count));
		} catch (ExecutionException e) {
			throw new DatabaseException(e);
		}
	}

	public List<PacketBbsResponse> getBbsResponses(int threadId, int count)
			throws DatabaseException {
		try {
			return bbsResponseCache.get(new BbsResponseCacheKey(threadId, count));
		} catch (ExecutionException e) {
			throw new DatabaseException(e);
		}
	}

	public void buildBbsThread(int bbsId, PacketBbsThread thread, PacketBbsResponse response)
			throws DatabaseException {
		database.buildBbsThread(bbsId, thread, response);
		bbsThreadCache.invalidateAll();
		bbsResponseCache.invalidateAll();
		numberOfBbsThreadsCache.invalidateAll();
	}

	public void writeToBbs(PacketBbsResponse response, boolean age) throws DatabaseException {
		database.writeToBbs(response, age);
		bbsResponseCache.invalidateAll();
		bbsThreadCache.invalidateAll();
		numberOfBbsThreadsCache.invalidateAll();
	}

	public int getNumberOfBbsThread(int bbsId) throws DatabaseException {
		try {
			return numberOfBbsThreadsCache.get(bbsId);
		} catch (ExecutionException e) {
			throw new DatabaseException(e);
		}
	}

	// //////////////////////////////////////////////////////////////////////////////
	// リンク
	private class LinkCacheKey {
		private final int start;
		private final int count;

		private LinkCacheKey(int start, int count) {
			this.start = start;
			this.count = count;
		}

		@Override
		public boolean equals(Object obj) {
			return obj instanceof LinkCacheKey && start == ((LinkCacheKey) obj).start
					&& count == ((LinkCacheKey) obj).count;
		}

		@Override
		public int hashCode() {
			int hash = start;
			hash = 31 * hash + count;
			return hash;
		}
	}

	private final LoadingCache<LinkCacheKey, List<PacketLinkData>> linkCache = build("linkCache",
			new CacheLoader<LinkCacheKey, List<PacketLinkData>>() {
				@Override
				public List<PacketLinkData> load(LinkCacheKey key) throws Exception {
					return database.getLinkDatas(key.start, key.count);
				}
			});

	private volatile int numberOfLinkData = Integer.MIN_VALUE;
	private final Object linkCacheLockObject = new Object();

	public void addLinkData(PacketLinkData linkData) throws DatabaseException {
		database.addLinkData(linkData);
		linkCache.invalidateAll();
		numberOfLinkData = Integer.MIN_VALUE;
	}

	public void updateLinkData(PacketLinkData linkData) throws DatabaseException {
		database.updateLinkData(linkData);
		linkCache.invalidateAll();
	}

	public void removeLinkData(int id) throws DatabaseException {
		database.removeLinkData(id);
		linkCache.invalidateAll();
		numberOfLinkData = Integer.MIN_VALUE;
	}

	public List<PacketLinkData> getLinkDatas(int start, int count) throws DatabaseException {
		try {
			return linkCache.get(new LinkCacheKey(start, count));
		} catch (ExecutionException e) {
			throw new DatabaseException(e);
		}
	}

	public int getNumberOfLinkDatas() throws DatabaseException {
		synchronized (linkCacheLockObject) {
			if (numberOfLinkData < 0) {
				numberOfLinkData = database.getNumberOfLinkDatas();
			}
			return numberOfLinkData;
		}
	}

	// //////////////////////////////////////////////////////////////////////////////
	// アクティブユーザ数
	private final Object lockNumberOfActiveUsers = new Object();
	private volatile int numberOfActiveUsers;

	public int getNumberOfActiveUsers() throws DatabaseException {
		// 初期値チェックのみで良いためダブルチェック
		if (numberOfActiveUsers == 0) {
			synchronized (lockNumberOfActiveUsers) {
				if (numberOfActiveUsers == 0) {
					numberOfActiveUsers = database.getNumberOfActiveUsers();
				}
			}
		}
		return numberOfActiveUsers;
	}

	private void updateNumberOfActiveUsers() throws DatabaseException {
		numberOfActiveUsers = database.getNumberOfActiveUsers();
	}

	// //////////////////////////////////////////////////////////////////////////////
	// テーマモード
	private final Object lockThemeModeQuery = new Object();
	private volatile Map<String, List<String>> themeModeQuery;

	public Map<String, List<String>> getThemeModeQueries() throws DatabaseException {
		synchronized (lockThemeModeQuery) {
			if (themeModeQuery == null) {
				themeModeQuery = database.getThemeModeQueries();
			}
			return themeModeQuery;
		}
	}

	@Override
	public void addThemeModeQuery(String theme, String query) throws DatabaseException {
		// ロックの順番に注意する
		synchronized (lockThemeModeQuery) {
			database.addThemeModeQuery(theme, query);
			themeModeQuery = null;
		}
	}

	@Override
	public void removeThemeModeQuery(String theme, String query) throws DatabaseException {
		synchronized (lockThemeModeQuery) {
			database.removeThemeModeQuery(theme, query);
			themeModeQuery = null;
		}
	}

	@Override
	public Map<String, IntArray> getThemeToProblems(Map<String, List<String>> themeAndQueryStrings)
			throws DatabaseException {
		return database.getThemeToProblems(themeAndQueryStrings);
	}

	// //////////////////////////////////////////////////////////////////////////////
	// テーマモードランキング
	private final Object lockThemeModeRanking = new Object();
	private volatile Map<String, List<PacketRankingData>> themeModeRanking;

	public Map<String, List<PacketRankingData>> getThemeModeRankingData() throws DatabaseException {
		// 初期値チェックのみで良いためダブルチェック
		if (themeModeRanking == null) {
			synchronized (lockThemeModeRanking) {
				if (themeModeRanking == null) {
					themeModeRanking = database.getThemeModeRankingData();
				}
			}
		}
		return themeModeRanking;
	}

	private void updateThemeModeRaking() throws DatabaseException {
		themeModeRanking = database.getThemeModeRankingData();
	}

	// //////////////////////////////////////////////////////////////////////////////
	// 最新の投稿問題
	private final Object lockLastestProblems = new Object();
	private volatile List<PacketProblem> lastestProblems;

	@Override
	public List<PacketProblem> getLastestProblems() throws DatabaseException {
		// 初期値チェックのみで良いためダブルチェック
		if (lastestProblems == null) {
			synchronized (lockLastestProblems) {
				if (lastestProblems == null) {
					lastestProblems = database.getLastestProblems();
				}
			}
		}
		return lastestProblems;
	}

	private void updateLastestProblems() throws DatabaseException {
		lastestProblems = database.getLastestProblems();
	}

	// //////////////////////////////////////////////////////////////////////////////
	// テーマモード編集者
	private final Object lockThemeModeEditors = new Object();
	private volatile List<PacketThemeModeEditor> themeModeEditors;

	@Override
	public List<PacketThemeModeEditor> getThemeModeEditors() throws DatabaseException {
		// 初期値チェックのみで良いためダブルチェック
		if (themeModeEditors == null) {
			synchronized (lockThemeModeEditors) {
				if (themeModeEditors == null) {
					themeModeEditors = database.getThemeModeEditors();
				}
			}
		}
		return themeModeEditors;
	}

	@Override
	public ThemeModeEditorStatus getThemeModeEditorsStatus(int userCode) throws DatabaseException {
		return database.getThemeModeEditorsStatus(userCode);
	}

	@Override
	public void updateThemeModeEdtorsStatus(int userCode, ThemeModeEditorStatus status)
			throws DatabaseException {
		database.updateThemeModeEdtorsStatus(userCode, status);
		themeModeEditors = null;
	}

	// //////////////////////////////////////////////////////////////////////////////
	// パスワード
	private final LoadingCache<String, String> passwordCache = build("passwordCache",
			new CacheLoader<String, String>() {
				@Override
				public String load(String key) throws Exception {
					return database.getPassword(key);
				}
			});

	@Override
	public String getPassword(String type) throws DatabaseException {
		try {
			return passwordCache.get(type);
		} catch (ExecutionException e) {
			throw new DatabaseException(e);
		}
	}

	// //////////////////////////////////////////////////////////////////////////////
	// 問題
	private volatile List<PacketProblemMinimum> problemMinimums;
	private final Object lockProblemMinimums = new Object();

	private void initializeProblemMinimumsIfNotInitialized() throws DatabaseException {
		if (problemMinimums == null) {
			synchronized (lockProblemMinimums) {
				if (problemMinimums == null) {
					final List<PacketProblemMinimum> problemMinimums = Lists.newArrayList();
					problemMinimums.add(new PacketProblemMinimum());
					// databaseのprocessProblems()を使用しないと無限再帰となってしまう
					database.processProblemMinimums(new ProblemMinimumProcessable() {
						@Override
						public void process(PacketProblemMinimum problem) throws Exception {
							problemMinimums.add(problem);
						}
					});

					Collections.sort(problemMinimums, new Comparator<PacketProblemMinimum>() {
						@Override
						public int compare(PacketProblemMinimum o1, PacketProblemMinimum o2) {
							return o1.id - o2.id;
						}
					});

					this.problemMinimums = problemMinimums;
				}
			}
		}
	}

	@Override
	public int addProblem(PacketProblem data) throws DatabaseException {
		initializeProblemMinimumsIfNotInitialized();

		data.id = database.addProblem(data);
		problemMinimums.add(data.asMinimum());
		return data.id;
	}

	@Override
	public List<PacketProblem> getProblem(Collection<Integer> ids) throws DatabaseException {
		initializeProblemMinimumsIfNotInitialized();

		return database.getProblem(ids);
	}

	@Override
	public PacketProblemMinimum getProblemMinimum(int problemId) throws DatabaseException {
		initializeProblemMinimumsIfNotInitialized();

		return problemMinimums.get(problemId);
	}

	@Override
	public void updateMinimumProblem(PacketProblemMinimum data) throws DatabaseException {
		initializeProblemMinimumsIfNotInitialized();

		database.updateMinimumProblem(data);
		problemMinimums.set(data.id, data);
	}

	@Override
	public void updateProblem(PacketProblem data) throws DatabaseException {
		initializeProblemMinimumsIfNotInitialized();

		database.updateProblem(data);
		problemMinimums.set(data.id, data);
	}

	@Override
	public void processProblems(ProblemProcessable processor) throws DatabaseException {
		initializeProblemMinimumsIfNotInitialized();

		database.processProblems(processor);
	}

	@Override
	public void processProblemMinimums(ProblemMinimumProcessable processer)
			throws DatabaseException {
		initializeProblemMinimumsIfNotInitialized();

		database.processProblemMinimums(processer);
	}

	@Override
	public List<PacketProblem> searchProblem(String query, String creator,
			boolean creatorPerfectMatching, Set<ProblemGenre> genres, Set<ProblemType> types,
			Set<RandomFlag> randomFlags) throws DatabaseException {
		initializeProblemMinimumsIfNotInitialized();

		return database.searchProblem(query, creator, creatorPerfectMatching, genres, types,
				randomFlags);
	}

	@Override
	public List<PacketProblem> searchSimilarProblemFromDatabase(PacketProblem problem)
			throws DatabaseException {
		initializeProblemMinimumsIfNotInitialized();

		return database.searchSimilarProblemFromDatabase(problem);
	}

	@VisibleForTesting
	void clearCache() {
		userDataCache.invalidateAll();
		rankingData = null;
		serverIgnoreUserCode = null;
		limitedUserCodes = null;
		limitedRemoteAddresses = null;
		bbsThreadCache.invalidateAll();
		bbsResponseCache.invalidateAll();
		numberOfBbsThreadsCache.invalidateAll();
		linkCache.invalidateAll();
		numberOfLinkData = Integer.MIN_VALUE;
		numberOfActiveUsers = 0;
		themeModeQuery = null;
		themeModeRanking = null;
		lastestProblems = null;
		themeModeEditors = null;
		passwordCache.invalidateAll();
		// 速度向上の為クリアしない
		// problemMinimums = null;
	}
}
